package matrix;

import java.io.*;
import java.util.ArrayList;

public class Matrix {
    private final double[][] data;

    public Matrix(int rows, int columnNumber) {
        data = new double[rows][columnNumber];
    }

    public Matrix(double[][] b) throws DimensionException{
        for (double[] aB : b) {
            if (aB.length != b[0].length) {
                throw new DimensionException("Number of elements in row is different");
            }
        }
        data = b;
    }

    public Matrix clone (){
        Matrix clone = new Matrix(getHeight(), getWidth());
        System.arraycopy(data, 0, clone.data, 0, getHeight());
        return clone;
    }
    public void set(int row, int column, double value) throws DimensionException {
        if (row >= 0 && column >= 0 && row < getHeight() && column < getWidth()) {
            data[row][column] = value;
        } else {
            throw new DimensionException("Out of matrix size. You try to set [" + row + "] [" + column +
                    "] element, but matrix is [" + getHeight() + "] [" + getWidth() + "]");
        }

    }

    public double get(int row, int column) throws DimensionException {
        if ((row < getHeight()) && (column < getWidth())) {
            return data[row][column];
        } else {
            throw new DimensionException("No such element [" + row + "] [" + column + "],because matrix is [" + getHeight() + "] [" + getWidth() + "]");
        }

    }

    public int getHeight(){
        return data.length;
    }

    public int getWidth(){
        return data[0].length;
    }

    public boolean equals(Matrix matrix){
        if (getHeight() != matrix.getHeight() || getWidth() != matrix.getWidth()){
            return false;
        }
        for (int x = 0; x < getHeight(); x++) {
            for (int y = x + 1; y < getWidth(); y++) {
                try {
                    if (Math.abs(get(x,y) - matrix.get(x, y)) > 0.0001){
                        return false;
                    }
                }catch (DimensionException dex){
                    dex.printStackTrace();
                }
            }
        }
        return true;
    }

    public boolean isSquare(){
        return getWidth() == getHeight();
    }

    /**
     * matrix is LowerTriangular if all elements above diagonal are zero.
     * @throws DimensionException if matrix isn't square.
     */
    public boolean isLowerTriangular() throws DimensionException{
        if (isSquare()){
            boolean isLT = true;
            for (int x = 0; x < getHeight(); x++){
                for (int y = x + 1; y < getWidth(); y++){
                    if (get(x, y)!= 0){
                        isLT = false;
                    }
                }
            }
            return isLT;
        } else throw new DimensionException("matrix isn't square");
    }

    /**
     * matrix is UpperTriangular if all elements below diagonal are zero.
     * @throws DimensionException if matrix isn't square.
     */
    public boolean isUpperTriangular() throws DimensionException{
        if (isSquare()){
            boolean isUT = true;
            for (int x = 0; x < getHeight(); x++){
                for (int y = 0; y < x; y++){
                    if (get(x, y) != 0){
                        isUT = false;
                    }
                }
            }
            return isUT;
        } else throw new DimensionException("matrix isn't square");
    }

    /**
     * matrix is Diagonal if all elements except diagonal are zero.
     * @throws DimensionException if matrix isn't square.
     */
    public boolean isDiagonal() throws DimensionException{
        return isLowerTriangular() && isUpperTriangular();
    }

    /*
    * Return minor matrix, that is the initial one except first row and x'th column.
    * Dimension of minor is one less then initial one.
    */
    private Matrix getMinor(int x){
        Matrix minor = new Matrix(getHeight() - 1, getWidth() - 1);
        for (int y = 1; y < getHeight(); y++){
            for (int z = 0; z < getWidth(); z++){
                if (z != x){
                    if (z > x){
                        try {
                            minor.set(y - 1, z - 1, get(y, z));
                        } catch (DimensionException dex){System.out.println(dex);}
                    } else {
                        try {
                            minor.set(y - 1, z, get(y, z));
                        } catch (DimensionException dex){System.out.println(dex);}
                    }
                }
            }
        }
        return minor;
    }

    /*
    * Returns minor matrix, that is the initial one except x'th row and y'th column.
    * Dimension of minor is one less then initial one.
    */
    private Matrix getMinor(int x, int y) {
        Matrix minor = new Matrix(getHeight() - 1, getWidth() - 1);
        int xNew;
        for (int xx = 0; xx < getHeight(); xx++) {
            if (xx != x) {
                if (xx > x) {
                    xNew = xx - 1;
                } else {
                    xNew = xx;
                }
                for (int yy = 0; yy < getWidth(); yy++) {
                    if (yy != y) {
                        if (yy > y) {
                            try {
                                minor.set(xNew, yy - 1, get(xx, yy));
                            } catch (DimensionException dex) {
                                System.out.println(dex);
                            }
                        } else {
                            try {
                                minor.set(xNew, yy, get(xx, yy));
                            } catch (DimensionException dex) {
                                System.out.println(dex);
                            }
                        }
                    }
                }
            }
        }
        return minor;
    }

    /**
     * Returns invertible matrix of initial one. Is used only for square matrix.
     * For calculations minors determinant method is used.
     * @throws DimensionException if matrix isn't square.
     */
    public Matrix getInvertible() throws DimensionException{
        if (isSquare()){
            double detM = det(this);
            Matrix result = new Matrix(getHeight(), getWidth());
            for (int x = 0; x < getHeight(); x++){
                for (int y = 0; y < getWidth(); y++){
                    result.set(y, x, Math.pow(-1, x + y) * det(getMinor(x, y)) / detM);
                }
            }
            return result;
        } else throw new DimensionException("matrix isn't square");
    }
    /**
     * Returns determinant of argument matrix. The method is based on recursion definition using minor determinant.
     * @return double value of determinant.
     * @throws DimensionException in case of non square incoming matrix.
     */
    public static double det(Matrix m) throws DimensionException{
        if (m.isSquare()){
            if (m.getHeight() == 1){
                return m.get(0, 0);
            } else {
                double aa = 0;
                for (int x = 0; x < m.getWidth(); x++){
                    Matrix minor = m.getMinor(x);
                    aa += Math.pow(-1, x) * m.get(0, x) * Matrix.det(minor);
                }
                return aa;
            }
        } else throw new DimensionException("matrix isn't square");
    }

    /**
     * Returns sum of the matrix and another matrix from argument.
     * @throws DimensionException in case, when number of columns or number of rows don't match.
     */
    public Matrix sum(Matrix sumWith) throws DimensionException {
        if (getWidth() != sumWith.getWidth() || getHeight() != sumWith.getHeight()) {
            throw new DimensionException("matrix size doesn't match");
        } else {
            double[][] result = new double[getHeight()][getWidth()];
            for (int x = 0; x < getHeight(); x++) {
                for (int y = 0; y < getWidth(); y++) {
                    result[x][y] = data[x][y] + sumWith.get(x, y);
                }
            }
            return new Matrix(result);
        }
    }

    /**
     * Sums the matrix with another matrix from argument. The source matrix is changed.
     * @throws DimensionException in case, when number of columns or number of rows don't match.
     */
    public void sumVoid (Matrix sumWith) throws DimensionException {
        if (getWidth() != sumWith.getWidth() || getHeight() != sumWith.getHeight()) {
            throw new DimensionException("matrix size doesn't match");
        } else {
            for (int x = 0; x < getHeight(); x++) {
                for (int y = 0; y < getWidth(); y++) {
                    data[x][y] = data[x][y] + sumWith.get(x, y);
                }
            }
        }
    }

    /**
     * Returns the result of multiplication the matrix to another matrix from argument.
     * @throws DimensionException if width of first matrix is not equal to hight of another one.
     */
    public Matrix multiplicate(Matrix multiplicateWith) throws DimensionException {
        if (getWidth() != multiplicateWith.getHeight()) {
            throw new DimensionException("matrix does not satisfy the criterion of compatibility");
        } else {
            double[][] result = new double[getHeight()][multiplicateWith.getWidth()];
            for (int x = 0; x < getHeight(); x++) {
                for (int y = 0; y < multiplicateWith.getWidth(); y++) {
                    for (int z = 0; z < getWidth(); z++) {
                        result[x][y] += data[x][z] * multiplicateWith.get(z, y);
                    }
                }
            }
            return new Matrix(result);
        }
    }

    /**
     * Multiplicates the matrix to another matrix from argument. The source matrix is changed.
     * @throws DimensionException if width of first matrix is not equal to hight of another one.
     */
    public void multiplicateVoid (Matrix multiplicateWith) throws DimensionException {
        if (getWidth() != multiplicateWith.getHeight()) {
            throw new DimensionException("matrix does not satisfy the criterion of compatibility");
        } else {
            double[][] result = new double[getHeight()][multiplicateWith.getWidth()];
            for (int x = 0; x < getHeight(); x++) {
                for (int y = 0; y < multiplicateWith.getWidth(); y++) {
                    for (int z = 0; z < getWidth(); z++) {
                        result[x][y] += data[x][z] * multiplicateWith.get(z, y);
                    }
                }
            }
            System.arraycopy(result, 0, data, 0, getHeight());
        }
    }

    /**
    * @return transpose matrix.
    */
    public Matrix transpose() {
        Matrix resultM = new Matrix(getWidth(), getHeight());
        for (int x = 0; x <getWidth(); x++) {
            for (int y = 0; y < getHeight(); y++) {
                try {
                    resultM.set(x, y, get(y, x));
                }catch (DimensionException dex){System.out.println(dex);}
            }
        }
        return resultM;
    }

    public String toString() {
        StringBuilder a = new StringBuilder();
        for (int x = 0; x < getHeight(); x++) {
            a.append("\n");
            for (int y = 0; y < getWidth(); y++) {
                a.append(data[x][y]).append(" ");
            }

        }
        return a.toString();
    }

    /**
    * Reads and returns matrix from file, if it can be found on the path from argument.
    * File must satisfy the following conditions: elements are separated by space " " and rows are separated by return "\n"
    * @throws java.io.IOException in case of file is unavailable or read function doesn't work.
    * @throws matrix.DimensionException in case of file contains not only numbers or number of elements in rows doesn't match.
    */
    public static Matrix readMatrixFromFile(FileReader stream) throws IOException, DimensionException{
        String line;
        ArrayList<double[]> body1 = new ArrayList<double[]>();
        BufferedReader reader = new BufferedReader(stream);
        while ((line = reader.readLine()) != null) {
            String[] lineStr = line.split(" ");
            double[] lineDoub = new double[lineStr.length];
            try {
                for (int x = 0; x < lineStr.length; x++) {
                    lineDoub[x] = Double.parseDouble(lineStr[x]);
                }
            }catch (RuntimeException run){
                throw new DimensionException ("File data can't be converted to the numbers");
            }
            body1.add(lineDoub);
        }
        double[][] body2 = new double[body1.size()][];
        for (int y = 0; y < body1.size(); y++) {
            body2[y] = body1.get(y);
        }
        return new Matrix(body2);
    }
}